(function umd(root, factory) {
  if (typeof module === 'object' && typeof exports === 'object')
    module.exports = factory()
  else if (typeof define === 'function' && define.amd)
    define([], factory)
  else
    root.httpVueLoader = factory()
})(this, function factory() {
  'use strict';

  var scopeIndex = 0;

  StyleContext.prototype = {

    withBase: function (callback) {

      var tmpBaseElt;
      if (this.component.baseURI) {

        // firefox and chrome need the <base> to be set while inserting or modifying <style> in a document.
        tmpBaseElt = document.createElement('base');
        tmpBaseElt.href = this.component.baseURI;

        var headElt = this.component.getHead();
        headElt.insertBefore(tmpBaseElt, headElt.firstChild);
      }

      callback.call(this);

      if (tmpBaseElt)
        this.component.getHead().removeChild(tmpBaseElt);
    },

    scopeStyles: function (styleElt, scopeName) {

      function process() {

        var sheet = styleElt.sheet;
        var rules = sheet.cssRules;

        for (var i = 0; i < rules.length; ++i) {

          var rule = rules[i];
          if (rule.type !== 1)
            continue;

          var scopedSelectors = [];

          rule.selectorText.split(/\s*,\s*/).forEach(function (sel) {

            scopedSelectors.push(scopeName + ' ' + sel);
            var segments = sel.match(/([^ :]+)(.+)?/);
            scopedSelectors.push(segments[1] + scopeName + (segments[2] || ''));
          });

          var scopedRule = scopedSelectors.join(',') + rule.cssText.substr(rule.selectorText.length);
          sheet.deleteRule(i);
          sheet.insertRule(scopedRule, i);
        }
      }

      try {
        // firefox may fail sheet.cssRules with InvalidAccessError
        process();
      } catch (ex) {

        if (ex instanceof DOMException && ex.code === DOMException.INVALID_ACCESS_ERR) {

          styleElt.sheet.disabled = true;
          styleElt.addEventListener('load', function onStyleLoaded() {

            styleElt.removeEventListener('load', onStyleLoaded);

            // firefox need this timeout otherwise we have to use document.importNode(style, true)
            setTimeout(function () {

              process();
              styleElt.sheet.disabled = false;
            });
          });
          return;
        }

        throw ex;
      }
    },

    compile: function () {

      var hasTemplate = this.template !== null;

      var scoped = this.elt.hasAttribute('scoped');

      if (scoped) {

        // no template, no scopable style needed
        if (!hasTemplate)
          return;

        // firefox does not tolerate this attribute
        this.elt.removeAttribute('scoped');
      }

      this.withBase(function () {

        this.component.getHead().appendChild(this.elt);
      });

      if (scoped)
        this.scopeStyles(this.elt, '[' + this.component.getScopeId() + ']');

      return Promise.resolve();
    },

    getContent: function () {

      return this.elt.textContent;
    },

    setContent: function (content) {

      this.withBase(function () {

        this.elt.textContent = content;
      });
    }
  };

  function StyleContext(component, elt) {

    this.component = component;
    this.elt = elt;
  }


  ScriptContext.prototype = {

    getContent: function () {

      return this.elt.textContent;
    },

    setContent: function (content) {

      this.elt.textContent = content;
    },

    compile: function (module) {

      var childModuleRequire = function (childURL) {

        return httpVueLoader.require(resolveURL(this.component.baseURI, childURL));
      }.bind(this);

      var childLoader = function (childURL, childName) {

        return httpVueLoader(resolveURL(this.component.baseURI, childURL), childName);
      }.bind(this);

      try {
        Function('exports', 'require', 'httpVueLoader', 'module', this.getContent()).call(this.module.exports, this.module.exports, childModuleRequire, childLoader, this.module);
      } catch (ex) {

        if (!('lineNumber' in ex)) {

          return Promise.reject(ex);
        }
        var vueFileData = responseText.replace(/\r?\n/g, '\n');
        var lineNumber = vueFileData.substr(0, vueFileData.indexOf(script)).split('\n').length + ex.lineNumber - 1;
        throw new (ex.constructor)(ex.message, url, lineNumber);
      }

      return Promise.resolve(this.module.exports)
        .then(httpVueLoader.scriptExportsHandler.bind(this))
        .then(function (exports) {

          this.module.exports = exports;
        }.bind(this));
    }
  };

  function ScriptContext(component, elt) {

    this.component = component;
    this.elt = elt;
    this.module = { exports: {} };
  }


  TemplateContext.prototype = {

    getContent: function () {

      return this.elt.innerHTML;
    },

    setContent: function (content) {

      this.elt.innerHTML = content;
    },

    getRootElt: function () {

      var tplElt = this.elt.content || this.elt;

      if ('firstElementChild' in tplElt)
        return tplElt.firstElementChild;

      for (tplElt = tplElt.firstChild; tplElt !== null; tplElt = tplElt.nextSibling)
        if (tplElt.nodeType === Node.ELEMENT_NODE)
          return tplElt;

      return null;
    },

    compile: function () {

      return Promise.resolve();
    }
  };

  function TemplateContext(component, elt) {

    this.component = component;
    this.elt = elt;
  }



  Component.prototype = {

    getHead: function () {

      return document.head || document.getElementsByTagName('head')[0];
    },

    getScopeId: function () {

      if (this._scopeId === '') {

        this._scopeId = 'data-s-' + (scopeIndex++).toString(36);
        this.template.getRootElt().setAttribute(this._scopeId, '');
      }
      return this._scopeId;
    },

    load: function (componentURL) {

      return httpVueLoader.httpRequest(componentURL)
        .then(function (responseText) {

          this.baseURI = componentURL.substr(0, componentURL.lastIndexOf('/') + 1);
          var doc = document.implementation.createHTMLDocument('');

          // IE requires the <base> to come with <style>
          doc.body.innerHTML = (this.baseURI ? '<base href="' + this.baseURI + '">' : '') + responseText;

          for (var it = doc.body.firstChild; it; it = it.nextSibling) {

            switch (it.nodeName) {
              case 'TEMPLATE':
                this.template = new TemplateContext(this, it);
                break;
              case 'SCRIPT':
                this.script = new ScriptContext(this, it);
                break;
              case 'STYLE':
                this.styles.push(new StyleContext(this, it));
                break;
            }
          }

          return this;
        }.bind(this));
    },

    _normalizeSection: function (eltCx) {

      var p;

      if (eltCx === null || !eltCx.elt.hasAttribute('src')) {

        p = Promise.resolve(null);
      } else {

        p = httpVueLoader.httpRequest(eltCx.elt.getAttribute('src'))
          .then(function (content) {

            eltCx.elt.removeAttribute('src');
            return content;
          });
      }

      return p
        .then(function (content) {

          if (eltCx !== null && eltCx.elt.hasAttribute('lang')) {

            var lang = eltCx.elt.getAttribute('lang');
            eltCx.elt.removeAttribute('lang');
            return httpVueLoader.langProcessor[lang.toLowerCase()].call(this, content === null ? eltCx.getContent() : content);
          }
          return content;
        }.bind(this))
        .then(function (content) {

          if (content !== null)
            eltCx.setContent(content);
        });
    },

    normalize: function () {

      return Promise.all(Array.prototype.concat(
        this._normalizeSection(this.template),
        this._normalizeSection(this.script),
        this.styles.map(this._normalizeSection)
      ))
        .then(function () {

          return this;
        }.bind(this));
    },

    compile: function () {

      return Promise.all(Array.prototype.concat(
        this.template && this.template.compile(),
        this.script && this.script.compile(),
        this.styles.map(function (style) { return style.compile(); })
      ))
        .then(function () {

          return this;
        }.bind(this));
    }
  };

  function Component(name) {

    this.name = name;
    this.template = null;
    this.script = null;
    this.styles = [];
    this._scopeId = '';
  }

  function identity(value) {

    return value;
  }

  function parseComponentURL(url) {

    var comp = url.match(/(.*?)([^/]+?)\/?(\.vue)?(\?.*|#.*|$)/);
    return {
      name: comp[2],
      url: comp[1] + comp[2] + (comp[3] === undefined ? '/index.vue' : comp[3]) + comp[4]
    };
  }

  function resolveURL(baseURL, url) {

    if (url.substr(0, 2) === './' || url.substr(0, 3) === '../') {
      return baseURL + url;
    }
    return url;
  }


  httpVueLoader.load = function (url, name) {

    return function () {

      return new Component(name).load(url)
        .then(function (component) {

          return component.normalize();
        })
        .then(function (component) {

          return component.compile();
        })
        .then(function (component) {

          var exports = component.script !== null ? component.script.module.exports : {};

          if (component.template !== null)
            exports.template = component.template.getContent();

          if (exports.name === undefined)
            if (component.name !== undefined)
              exports.name = component.name;

          exports._baseURI = component.baseURI;

          return exports;
        });
    };
  };


  httpVueLoader.register = function (Vue, url) {

    var comp = parseComponentURL(url);
    Vue.component(comp.name, httpVueLoader.load(comp.url));
  };

  httpVueLoader.install = function (Vue) {

    Vue.mixin({

      beforeCreate: function () {

        var components = this.$options.components;

        for (var componentName in components) {

          if (typeof (components[componentName]) === 'string' && components[componentName].substr(0, 4) === 'url:') {

            var comp = parseComponentURL(components[componentName].substr(4));

            var componentURL = ('_baseURI' in this.$options) ? resolveURL(this.$options._baseURI, comp.url) : comp.url;

            if (isNaN(componentName))
              components[componentName] = httpVueLoader.load(componentURL, componentName);
            else
              components[componentName] = Vue.component(comp.name, httpVueLoader.load(componentURL, comp.name));
          }
        }
      }
    });
  };

  httpVueLoader.require = function (moduleName) {

    return window[moduleName];
  };

  httpVueLoader.httpRequest = function (url) {

    return new Promise(function (resolve, reject) {

      var xhr = new XMLHttpRequest();
      xhr.open('GET', url);

      xhr.onreadystatechange = function () {

        if (xhr.readyState === 4) {

          if (xhr.status >= 200 && xhr.status < 300)
            resolve(xhr.responseText);
          else
            reject(xhr.status);
        }
      };

      xhr.send(null);
    });
  };

  httpVueLoader.langProcessor = {
    html: identity,
    js: identity,
    css: identity
  };

  httpVueLoader.scriptExportsHandler = identity;

  function httpVueLoader(url, name) {

    var comp = parseComponentURL(url);
    return httpVueLoader.load(comp.url, name);
  }

  return httpVueLoader;
});